﻿using System;
using System.Diagnostics;
using System.Reflection;
using System.Security;
using System.Security.Permissions;
using System.Security.Principal;
using System.Threading;
using System.IO;

namespace Logging
{

    /// <summary>
    /// This class provides a generic event logging system that wraps the existing framework classes.
    /// A method is provided for writing to a custom log file using a custom source.
    /// Note: Administrative rights are required to create new log files or sources. You must
    /// to create these objects during installation before attempting to use this class with them.
    /// To be graceful, if this class detects non-admin rights it will write to the Application log 
    /// using the Application source and note the 'preferred' log and source in the message.
    /// </summary>
    public class EventLogHelper
    {
        /// <summary>
        /// This enumeration defines three different levels of privileges the current thread can be executing under.
        /// </summary>
        private enum PrivilegeLevels
        {
            User = 0,
            Administrator = 1,
            System = 2
        }

        //declare EventLog objects and instantiate in constructor.
        private EventLog _customLog;
        private static EventLog _appLog;

        //use for thread safety in static members
        private static object syncRoot = new object();

        private FileInfo _logFile;

        private PrivilegeLevels _privilegeLevel = PrivilegeLevels.User;

        #region Constructors

        /// <summary>
        /// This constructor requires the name of the Log and Source to be given to create custom log entries.
        /// If the SourceName already is registered in a different Log on this machine then it will use the other log name instead of the one provided.
        /// If the LogName provided is null, and the Source is already registered, it will use that Log.
        /// If the LogName provided is null and the Source is not registered already it will create both of them
        /// If the user doesn't have admin rights the LogName and SourceName must already exist.
        /// </summary>
        /// <param name="LogName">The name of the Log file to create.</param>
        /// <param name="SourceName">The name of the source to use when creating Entries.</param>
        /// <example><c>EventLogHelper _evh = new EventLogHelper("MyLogFile", "ThisApplicationName");</c></example>     
        public EventLogHelper(string LogName, string SourceName)
        {
            //determine privilege level
            _privilegeLevel = GetUserPrivilegeLevel();

            string _log = "";

            //only run if we have admin rights to avoid exception
            if (_privilegeLevel != PrivilegeLevels.User)
            {
                //get log name if source is already registered
                _log = EventLog.LogNameFromSourceName(SourceName, Environment.MachineName);
            }

            //create new EventLog object using parameters and local machine name. 
            //Use the registered logname if available, otherwise use what was passed in. If both are blank then use the Application log          

            string _useThisLog = (_log != "" ? _log : (LogName != "" ? LogName : "Application"));

            //create new eventlog object with whatever log name and source name was requested.
            _customLog = new EventLog(_useThisLog, Environment.MachineName, SourceName);
        }

        /// <summary>
        /// This constructor accepts a string with the path and filename of a log file to use.
        /// If there is no file one will be created.
        /// If the file cannot be created the log file will be created in the local application data folder for the user.
        /// Be sure to properly escape directory separators (i.e. <c>new EventLogHelper("c:\\temp\\test.log")</c> or <c>new EventLogHelper(@"c:\temp\temp.log")</c>
        /// </summary>
        /// <param name="LogFilePath">The absolute path and filename of the log file.</param>
        public EventLogHelper(string LogFilePath)
        {
            try
            {
                PathName _path = new PathName(LogFilePath);

                if (_path.IsValidFile() || !_path.InvalidAsFile())
                {
                    _logFile = new FileInfo(LogFilePath);
                }
            }
            catch (Exception ex)
            {
                //set this instance to null and write something to the Event Log               
                EventLogHelper.WriteApplicationEvent("Could not create log file using " + LogFilePath, EventLogEntryType.Error, "Application");
                //pass exception back up
                throw ex;
            }
        }
        #endregion


        #region WriteEvents
        /// <summary>
        /// Writes to this instance's custom log file.
        /// If the user doesn't have admin rights and the write attempt fails, this instance will use the 'Application' Log and Source instead until the source is changed. Pertinent information will be at the bottom of a message indicating it can't find the message source for eventID (0).
        /// Once this exception has occurred, future writes to the event log will ALWAYS go to to the 'failsafe' Application Log with Source=Application
        /// For this reason, make sure the Log and Source objects already exist before using this method without admin rights.
        /// </summary>
        /// <param name="Message">The message to record in the Event Log.</param>
        /// <param name="EntryType">The type of Entry to create (Information, Warning, or Error).</param>
        public void WriteCustomEvent(string Message, EventLogEntryType EntryType)
        {
            try
            {
                _customLog.WriteEntry(Message, EntryType);
            }
            catch (SecurityException)
            {
                //user is non-admin and the specified log or source cannot be created.
                WriteEventFailure(Message, EntryType, ref _customLog);
            }
            catch (ArgumentException)
            {
                //mismatched log - source combination and user is non-admin
                //set log to blank and let it figure out what log to put it in
                _customLog.Log = "";
                this.WriteCustomEvent(Message, EntryType);
            }
            catch (Exception)
            {
                //catch any other exceptions and silently fail, writing to the application log
                EventLogHelper.WriteApplicationEvent("Could not create Event Log entry.\nMessage: " + Message, EventLogEntryType.Error, "Application");
            }
        }

        /// <summary>
        /// This method changes the EventLog instance it was passed to use the Application log with an Application source.
        /// This accommodates failures that arise when non-admins attempt to usea source that doesn't exist
        /// </summary>
        /// <param name="_message">The message to record in the Event Log.</param>
        /// <param name="_entryType">The type of Entry to create (Information, Warning, or Error).</param>
        /// <param name="_eventLog">A reference to the EventLog instance that will be altered.</param>
        private static void WriteEventFailure(string _message, EventLogEntryType _entryType, ref EventLog _eventLog)
        {
            //make sure this hasn't already run
            if (!(_eventLog.Log == "Application") && (_eventLog.Source == "Application"))
            {
                //use application log as a substitute with source=Application, which should already exist
                string _log = _eventLog.Log;
                string _source = _eventLog.Source;

                _eventLog.Log = "Application";
                _eventLog.Source = "Application";

                _eventLog.WriteEntry(string.Format("Failed to write event to Event Log {0} with Source {1}. The user may not have administrative rights.", _log, _source), EventLogEntryType.Warning);

                _eventLog.WriteEntry(string.Format("EventLog: {0}\r\nSource: {1}\r\nMessage: {2}", _log, _source, _message), _entryType);
            }
        }

        /// <summary>
        /// This method allows events to be written to the Application log. The caller must specify the source.
        /// If the source doesn't exist or is registered to a different Log it will be set to 'Application'.
        /// </summary>
        /// <param name="Message">The message to record in the Event Log.</param>
        /// <param name="EntryType">The type of Entry to create (Information, Warning, or Error).</param>
        /// <param name="Source">The Event Source to use (must be registered to the Application log)</param>
        public static void WriteApplicationEvent(string Message, EventLogEntryType EntryType, string Source)
        {
            lock (syncRoot)
            {
                //lazy instantiation
                if (_appLog == null)
                {
                    _appLog = new EventLog("Application", Environment.MachineName);
                }

                _appLog.Source = Source;

                try
                {
                    _appLog.WriteEntry(Message, EntryType);
                }
                catch (Exception)
                {
                    //on any failure write it to the Application Log using Application Source.
                    //failure is caused by non-admin rights AND attempting to create a new Source or using a register Source with the wrong Log
                    WriteEventFailure(Message, EntryType, ref _appLog);
                }
            }//exit lock
        }

        /// <summary>
        /// This method writes an error message to the log file specified by the _logfile FileInfo object
        /// </summary>
        public void WriteEventToFile(string Message, EventLogEntryType EntryType)
        {
            //todo: perhaps add more structure layout to this?
            if (_logFile != null)
            {
                //using (StreamWriter sw = _logFile.AppendText())
                using (System.Xml.XmlTextWriter xw = new System.Xml.XmlTextWriter(_logFile.Open(FileMode.Append, FileAccess.Write) , System.Text.Encoding.ASCII))
                {
                    try
                    {
                        //sw.WriteLine(EntryType.ToString() + ": " + Message);
                        //xw.WriteLine("{0} {1}: {2}", DateTime.Now, EntryType.ToString(), Message);

                        //xw.WriteElementString("Date", DateTime.Now.ToShortDateString() + " " + DateTime.Now.ToShortTimeString());
                        //xw.WriteElementString("EntryType", EntryType.ToString());
                        //xw.WriteElementString("Message", Message);

                        xw.WriteStartElement("Event");
                        
                        xw.WriteAttributeString("Date", DateTime.Now.ToShortDateString());
                        xw.WriteAttributeString("Time", DateTime.Now.ToShortTimeString());
                        xw.WriteAttributeString("EntryType", EntryType.ToString());
                        xw.WriteString(Message);

                        xw.WriteEndElement();
                        xw.WriteString("\n");
                        
                        xw.Close();
                    }
                    catch (Exception ex)
                    {
                        EventLogHelper.WriteApplicationEvent("Could not write to log file.\nMessage:  " + Message + "\nException:" + ex.Message, EventLogEntryType.Error, "Application");
                    }
                }
            }
        }

        /// <summary>
        /// Writes an event to either the Event Log or a log file, depending on how the EventLogHelper was instantiated.
        /// If a failure occurs, the message will be written to the debug console.
        /// </summary>
        /// <param name="Message">The message to record in this EventLogHelper's log system.</param>
        /// <param name="EntryType">The type of Entry to create (Information, Warning, or Error).</param>
        public void WriteEvent(string Message, EventLogEntryType EntryType)
        {
            try
            {
                //write to the log file if available
                if (_logFile != null)
                {
                    WriteEventToFile(Message, EntryType);
                }
                else
                    //no log file, see if Event Log is being used
                    if (_customLog != null)
                    {
                        WriteCustomEvent(Message, EntryType);
                    }
                    else
                    {
                        //shouldn't happen, but if neither the log file or Event Log is available, write it to the application log
                        WriteApplicationEvent(Message, EntryType, "Application");
                    }
            }
            catch (Exception)
            {
               //silently fail
                Debug.WriteLine(EntryType.ToString() + ": " + Message);
            }

        }

        #endregion

        /// <summary>
        /// This method gets one of three privilege levels the current thread is executing with.
        /// </summary>
        /// <returns>System or Administrator if explicitly qualified, otherwise User (default)</returns>
        private PrivilegeLevels GetUserPrivilegeLevel()
        {
            //make sure this thread uses a WindowsPrincipal object for their CurrentThread property
            AppDomain.CurrentDomain.SetPrincipalPolicy(PrincipalPolicy.WindowsPrincipal);

            WindowsPrincipal _principal = (WindowsPrincipal)Thread.CurrentPrincipal;
            WindowsIdentity _identity = (WindowsIdentity)_principal.Identity;

            //see if identity is SYSTEM, indicating this code is invoked from a service
            if (_identity.IsSystem)
            {
                return PrivilegeLevels.System;
            }
            else
            {
                return _principal.IsInRole(WindowsBuiltInRole.Administrator) ?
                    PrivilegeLevels.Administrator : PrivilegeLevels.User;
            }
        }


    }//end class

    internal class PathName
    {
        #region Properties
        
        /// <summary>
        /// This Pathname exists as an actual directory
        /// </summary>
        public bool ExistsAsDirectory { get; private set; }

        /// <summary>
        /// This Pathname exists as an actual file
        /// </summary>
        public bool ExistsAsFile { get; private set; }

        /// <summary>
        /// The portion of the PathName that appears to be a directory
        /// </summary>
        public string DirectorySection { get; private set; }

        /// <summary>
        /// The portion of the Pathname that appears to be a file name
        /// </summary>
        public string FileNameSection { get; private set; }

        /// <summary>
        /// True if the Directory Section contains invalid characters
        /// </summary>
        public bool InvalidDirSectionChars { get; private set; }

        /// <summary>
        /// True if the File Section contains invalid characters
        /// </summary>
        public bool InvalidFileSectionChars { get; private set; }

        /// <summary>
        /// True if this PathName cannot exist as a directory.
        /// </summary>
        public bool InvalidAsDirectory { get; private set; }

        /// <summary>
        /// The full path name represented by this object
        /// </summary>
        public string FullPath { get; private set; }

        #endregion

        /// <summary>
        /// This constructor requires a pathname. It sets the properties of this instance accordingly. The path name cannot be changed.
        /// </summary>
        /// <param name="FullPathName">The string representing the full path name to represent.</param>
        public PathName(String FullPathName)
        {
            this.FullPath = FullPathName;

            this.InvalidAsDirectory = HasInvalidChars(this.FullPath, Path.GetInvalidPathChars());

            //Set DirectorySection to "" if it can't be a directory; otherwise an exception would be thrown
            this.DirectorySection = this.InvalidAsDirectory ? "" : Path.GetDirectoryName(this.FullPath);
            this.FileNameSection = Path.GetFileName(this.FullPath);

            this.InvalidDirSectionChars = HasInvalidChars(this.DirectorySection, Path.GetInvalidPathChars());
            this.InvalidFileSectionChars = HasInvalidChars(this.FileNameSection, Path.GetInvalidFileNameChars());

            //check for invalid directory first
            if (!this.InvalidDirSectionChars)
            {
                this.ExistsAsDirectory = Directory.Exists(this.FullPath);
            }

            //check for invalid filename first
            if (!this.InvalidFileSectionChars)
            {
                this.ExistsAsFile = File.Exists(this.FullPath);
            }
#if DEBUG
            Debug.WriteLine("Is valid:" + IsValidFile());
            Debug.WriteLine(ExistsAsDirectory);
            Debug.WriteLine(ExistsAsFile);
            Debug.WriteLine(InvalidAsDirectory);
#endif
        }

        /// <summary>
        /// This shows whether the PathName object represents an existing file.
        /// </summary>
        /// <returns>True if the FullPath refers to an existing file, otherwise false.</returns>
        public bool IsValidFile()
        {
            return ((!this.ExistsAsDirectory) && (this.ExistsAsFile));
        }

        /// <summary>
        /// This shows whether the PathName object can be used to create a new file.
        /// </summary>
        /// <returns>True if the FullPath is not a directory, empty string, or contains illegal characters.</returns>
        public bool InvalidAsFile()
        {
            return ((this.ExistsAsDirectory) || (string.IsNullOrEmpty(this.FileNameSection)) || this.InvalidFileSectionChars);
        }

        /// <summary>
        /// This method loops through an array of chars and determines if the _path string contains them
        /// </summary>
        /// <param name="_path">The path string to test for invalid characters</param>
        /// <param name="_invalidChars">An char array of invalid characters to test for.</param>
        /// <returns>True if one or more invalid characters are found (or if the path string is empty), otherwise false.</returns>
        private bool HasInvalidChars(string _path, char[] _invalidChars)
        {
            //todo: clunky, but works.
            int i = 0;

            if (String.IsNullOrEmpty(_path))
            {
                i = -1;
            }
            else
            {
                //loop through each invalid character and see if the path contains it. In the end if the counter is 0 then no invalid characters are found.
                foreach (char c in _invalidChars)
                {
                    i += _path.Contains(c.ToString()) ? 1 : 0;
                }
            }
            return (i != 0);
        }
    }//end class
}//end namespace
